﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using ConciseMarkupLanguage.Ast;
using ConciseMarkupLanguage.Filters;
using ConciseMarkupLanguage.Tags;
using ConciseMarkupLanguage.Terminals;
using ConciseMarkupLanguage.Utils;
using Irony.Ast;
using Irony.Interpreter;
using Irony.Parsing;

namespace ConciseMarkupLanguage
{
    [Language("Concise Markup Language", "0.1", "Concise HTML generator.")]
    public class CmlGrammar : Grammar
    {
        public static readonly NamespaceDictionary<MethodInfo> TemplateTags = new NamespaceDictionary<MethodInfo>();
        public static readonly NamespaceDictionary<MethodInfo> Filters = new NamespaceDictionary<MethodInfo>();

        static CmlGrammar()
        {
            var tags = from assembly in AppDomain.CurrentDomain.GetAssemblies()
                let types = assembly.GetTypes()
                let methods = types.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static))
                from method in methods
                let attributes = method.GetCustomAttributes(true).OfType<TemplateTagAttribute>()
                where attributes.Any()
                select new {Method = method, Attribute = attributes.Single()};

            foreach (var t in tags)
            {
                TemplateTags.Add(t.Attribute.Namespace ?? t.Method.DeclaringType.Name, t.Attribute.Name ?? t.Method.Name, t.Method);
            }

            var filters = from assembly in AppDomain.CurrentDomain.GetAssemblies()
                let types = assembly.GetTypes()
                let methods = types.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static))
                from method in methods
                let attributes = method.GetCustomAttributes(true).OfType<FilterAttribute>()
                where attributes.Any()
                select new {Method = method, Attribute = attributes.Single()};

            foreach (var f in filters)
            {
                Filters.Add(f.Attribute.Namespace ?? f.Method.DeclaringType.Name, f.Attribute.Name ?? f.Method.Name, f.Method);
            }
        }

        public CmlGrammar() : base(caseSensitive:false)
        {
            GrammarComments = @"Long descrip.";

            #region Terminals
            var NUMBER = new NumberLiteral("number");
            NUMBER.DefaultIntTypes = new[] { TypeCode.Int32, TypeCode.UInt32, TypeCode.Int64, TypeCode.UInt64 };
            NUMBER.DefaultFloatType = TypeCode.Double;
            NUMBER.AddPrefix("0x", NumberOptions.Hex);
            NUMBER.AddSuffix("u", TypeCode.UInt32, TypeCode.UInt64);
            NUMBER.AddSuffix("l", TypeCode.Int64, TypeCode.UInt64);
            NUMBER.AddSuffix("ul", TypeCode.UInt64);
            NUMBER.AddSuffix("f", TypeCode.Single);
            NUMBER.AddSuffix("d", TypeCode.Double);
            NUMBER.AddSuffix("m", TypeCode.Decimal);

            var STRING = new StringLiteral("string");
            STRING.AddStartEnd("\"", StringOptions.AllowsAllEscapes);
            STRING.AddStartEnd("\"\"\"", StringOptions.AllowsAllEscapes | StringOptions.AllowsLineBreak);
            STRING.AddPrefix("r", StringOptions.NoEscapes);
            STRING.AddPrefix("@", StringOptions.NoEscapes | StringOptions.AllowsLineBreak | StringOptions.AllowsDoubledQuote);

            var charLiteral = new StringLiteral("char", "'", StringOptions.IsChar);

            
            var IDENT = new IdentifierTerminal("identifier", IdOptions.AllowsEscapes | IdOptions.CanStartWithEscape);
            IDENT.StartCharCategories.AddRange(new[]{
                UnicodeCategory.UppercaseLetter,
                UnicodeCategory.LowercaseLetter,
                UnicodeCategory.TitlecaseLetter,
                UnicodeCategory.ModifierLetter,
                UnicodeCategory.OtherLetter,
                UnicodeCategory.LetterNumber
            });
            IDENT.CharCategories.AddRange(IDENT.StartCharCategories);
            IDENT.CharCategories.AddRange(new[]{
                UnicodeCategory.DecimalDigitNumber,
                UnicodeCategory.ConnectorPunctuation,
                UnicodeCategory.SpacingCombiningMark,
                UnicodeCategory.NonSpacingMark,
                UnicodeCategory.Format
            });
            IDENT.CharsToRemoveCategories.Add(UnicodeCategory.Format);

            var FREE_TEXT = new FreeTextLiteral("free_text", ")");

            var SINGLE_LINE_COMMENT = new CommentTerminal("single_line_comment", "//", "\r", "\n", "\u2085", "\u2028", "\u2029");
            var MULTI_LINE_COMMENT = new CommentTerminal("multiline_comment", "/*", "*/");
            
            NonGrammarTerminals.Add(SINGLE_LINE_COMMENT);
            NonGrammarTerminals.Add(MULTI_LINE_COMMENT);

            var VARIABLE = new RegexBasedTerminal("variable", @"(?-i)\$\w+");
            var ATTR_NAME = new RegexBasedTerminal("attr_name", @"(?i)[a-z_]\w*");
            #endregion

            #region Operators
            RegisterOperators(1, "||");
            RegisterOperators(2, "&&");
            RegisterOperators(3, "|");
            RegisterOperators(4, "^");
            RegisterOperators(5, "&");
            RegisterOperators(6, "==", "!=");
            RegisterOperators(7, "<", ">", "<=", ">=");
            RegisterOperators(8, "<<", ">>");
            RegisterOperators(9, "+", "-");
            RegisterOperators(10, "*", "/", "%");
            RegisterOperators(11, Associativity.Right, "**");
            RegisterOperators(12, ".");
            #endregion

            #region Non-Terminals
            var assignment = TransientNonTerminal("assignment");
            var assignment_op = TransientNonTerminal("assignment_op");
            var attr = TransientNonTerminal("attr", typeof(AttrNode));//
            var attr_args = TransientNonTerminal("attr_args");
            var attr_args_opt = TransientNonTerminal("attr_args_opt");
            var attr_star = TransientNonTerminal("attr_star", typeof(AttrListNode));
            var block = TransientNonTerminal("block");
            var boolean = TransientNonTerminal("boolean", typeof(BooleanNode));
            var comparison = TransientNonTerminal("comparison", typeof(GenericNode));//
            var comparison_op = TransientNonTerminal("compare_ops", typeof(GenericNode));
            var custom_directive = TransientNonTerminal("custom_directive", typeof(GenericNode));
            var custom_directive_kw = TransientNonTerminal("custom_directive_kw");
            var directive_blk = TransientNonTerminal("directive_blk", typeof(GenericNode));
            var filter = TransientNonTerminal("filter", typeof(FilterNode));//
            var filter_kw = TransientNonTerminal("filter_kw");
            var filter_list = TransientNonTerminal("filter_list", typeof(GenericNode));
            var for_loop = TransientNonTerminal("for_loop", typeof(GenericNode));
            var foreach_as = TransientNonTerminal("foreach/as", typeof(ForeachNode));
            var foreach_in = TransientNonTerminal("foreach/in", typeof(ForeachNode));
            var free_args = TransientNonTerminal("free_args");
            var free_args_opt = TransientNonTerminal("free_args_opt");
            var html_tag_kw = TransientNonTerminal("html_tag_kw");
            var if_condition = TransientNonTerminal("if_condition", typeof(GenericNode));
            var if_else = TransientNonTerminal("if_else", typeof(GenericNode));
            var inc_or_dec_kw = TransientNonTerminal("inc_or_dec_kw");
            var list_obj = TransientNonTerminal("list_obj");
            var number_obj = TransientNonTerminal("number_obj");
            var op_assignment = TransientNonTerminal("op_assignment", typeof(GenericNode));
            var pipe_filter = TransientNonTerminal("pipe_filter");
            var post_inc_dec = TransientNonTerminal("post_inc_dec", typeof(GenericNode));
            var pre_inc_dec = TransientNonTerminal("pre_inc_dec", typeof(GenericNode));
            var preset_directive = TransientNonTerminal("preset_directive");
            var simple_assignment = TransientNonTerminal("simple_assignment", typeof(GenericNode));
            var statement = TransientNonTerminal("statement");
            var statement_list = TransientNonTerminal("statement_list");
            var statement_star = TransientNonTerminal("statement_star", typeof(Ast.StatementListNode));
            var tag = TransientNonTerminal("tag", typeof(TagNode));
            var tag_blk = TransientNonTerminal("tag_blk", typeof(TagBlockNode));
            var term = TransientNonTerminal("term");
            var term_filter = TransientNonTerminal("term_filter", typeof(TermFilterNode));
            var term_list = TransientNonTerminal("term_list");
            var term_simple = TransientNonTerminal("term_simple", typeof(TermSimpleNode));
            var term_star = TransientNonTerminal("term_star", typeof(TermListNode));
            var variable = TransientNonTerminal("variable", typeof(VariableNode));
            var filter_args = TransientNonTerminal("filter_args");
            var filter_args_opt = TransientNonTerminal("filter_args");
            #endregion

            #region Rules
            filter_kw.Rule = Filters.Aggregate(first => (BnfExpression)ToTerm(first.Key), (seed, item) => seed | item.Key);
            html_tag_kw.Rule = TemplateTags.Aggregate(first => (BnfExpression)ToTerm(first.Key), (seed, item) => seed | item.Key);

            comparison_op.Rule = ToTerm("==") | "<=" | ">=" | "!=" | "^=" | "$=" | "~=" | "<" | ">";

            variable.Rule = VARIABLE;

            // terms
            term_simple.Rule = NUMBER | STRING | variable | boolean | "null" | term_list;
            term_star.Rule = MakeStarRule(term_star, ToTerm(","), term);
            term_list.Rule = "[" + term_star + "]";
            term_filter.Rule = term + "|" + filter;
            term.Rule = term_simple | term_filter;

            // types
            boolean.Rule = ToTerm("true") | "false";
            list_obj.Rule = term_filter | STRING | variable | term_list;
            
            // filters
            free_args.Rule = "(" + FREE_TEXT + ")";
            free_args_opt.Rule = free_args | Empty;
            filter_args.Rule = "(" + term_star + ")";
            filter_args_opt.Rule = filter_args | Empty;
            filter.Rule = filter_kw + filter_args_opt;
           
            // tags
            attr.Rule = ATTR_NAME + "=" + term;
            attr_star.Rule = MakeStarRule(attr_star, ToTerm(","), attr);
            attr_args.Rule = "(" + attr_star + ")";
            attr_args_opt.Rule = attr_args | Empty;
            tag_blk.Rule = html_tag_kw + attr_args_opt + block;

            // comparison
            comparison.Rule = term + comparison_op + term;

            // assignment
            number_obj.Rule = NUMBER | variable;
            inc_or_dec_kw.Rule = ToTerm("++") | "--";
            pre_inc_dec.Rule = inc_or_dec_kw + variable;
            post_inc_dec.Rule = variable + inc_or_dec_kw;
            assignment_op.Rule = ToTerm("+=") | "-=" | "*=" | "/=" | "=";
            op_assignment.Rule = variable + assignment_op + term;
            simple_assignment.Rule = variable + "=" + term;
            assignment.Rule = op_assignment | pre_inc_dec | post_inc_dec;

            // directives
            foreach_in.Rule = ToTerm("foreach") + "(" + variable + "in" + list_obj + ")" + block;
            foreach_as.Rule = ToTerm("foreach") + "(" + list_obj + "as" + variable + ")" + block;
            for_loop.Rule = ToTerm("for") + "(" + simple_assignment + ";" + comparison + ";" + assignment + ")" + block;
            if_condition.Rule = ToTerm("if") + "(" + comparison + ")" + block;
            if_else.Rule = ToTerm("if") + "(" + comparison + ")" + block + PreferShiftHere() + "else" + block;
            directive_blk.Rule = foreach_in | foreach_as | for_loop | if_else | if_condition;

            // block
            statement.Rule = tag_blk | directive_blk | term;
            statement_star.Rule = MakeStarRule(statement_star, statement);
            statement_list.Rule = "{" + statement_star + "}";
            block.Rule = statement_list | statement | ";";
            
            // root
            Root = tag_blk;
            #endregion

            #region AST
            MarkPunctuation("{", "}", "(", ")", "[", "]",  "|", ";", ":");
            RegisterBracePair("{", "}");
            RegisterBracePair("(", ")");
            RegisterBracePair("[", "]");
            LanguageFlags = LanguageFlags.CreateAst | LanguageFlags.CanRunSample;
            #endregion
        }

        NonTerminal TransientNonTerminal(string name, Type nodeType = null, bool? isTransient = null)
        {
            var t = new NonTerminal(name, nodeType);
            if ((isTransient.HasValue && isTransient.Value) || (!isTransient.HasValue && nodeType == null))
                MarkTransient(t);
            return t;
        }

        BnfExpression Optional(BnfTerm t)
        {
            return t | Empty;
        }

        public override string RunSample(ParseTree parsedSample)
        {
            Debug.WriteLine("----------------------------------");
            var language = new LanguageData(this);
            var runtime = CreateRuntime(language);
            var context = new EvaluationContext(runtime);
            context.Globals["pi"] = Math.PI;
            var astNode = (AstNode)parsedSample.Root.AstNode;
            astNode.Evaluate(context, AstMode.Read);
            var html = context.OutputBuffer.ToString();
            var path = Path.Combine(Path.GetTempPath(), "cmlrun.html");
            File.WriteAllText(path, html);
            Process.Start(path);
            return XElement.Parse(html).ToString();
        }
    }
}
